<?php  if (!defined('BASEPATH')) exit('No direct script access allowed');

/*
    Class ActiveRecord
    ActiveRecord class to map database tables to PHP objects
*/
class ActiveRecord {
    
    protected $_tableName;
    protected $_keyField = 'id';

    private $_data = array();
    private $_modifiedFields = array();
    private $_parentObjects = array();

    /*
        Property: _status
        This identifies the current status of the object
        
        Options:
            -1  - (new) The object is new and has never been commited to the database
            0   - (dirty) The object was loaded from the database but has been modified
            1   - (clean) The object is stright from the database and is not modified
    */
    private $_status = -1;

    protected $_ci;
    private $db;

	protected $validation;

    /*
        Event methods
        These should be overridden by the extending class. If a method returns
        (bool) FALSE then the actual event will be skipped
    */
    protected function preLoad()    { return TRUE; }
    protected function postLoad()   { return TRUE; }
    protected function preInsert()  { return TRUE; }
    protected function postInsert() { return TRUE; }
    protected function preUpdate()  { return TRUE; }
    protected function postUpdate() { return TRUE; }
    protected function preDelete()  { return TRUE; }
    protected function postDelete() { return TRUE; }

    /*
        Function: validateInsert
    */
    protected function validateInsert()
    {
        return TRUE;
    }
    
    /*
        Function: validateUpdate
    */
    protected function validateUpdate()
    {
        return TRUE;        
    }

    /*
        Constructor: __construct
        Gets the database instance and attempts to load a record if an id is
        provided
        
        Arguments:
            id  - _keyField value to load
    */
    public function __construct($id=null)
    {
        $this->_ci = &get_instance();
        $this->db = $this->_ci->db;
		$this->validation = new ActiveRecordValidation;
		
        if ($id) return $this->load($id);
    }
    
    /*
        Function: __get
        Return the value of the key in _data
    */
    public function __get($key)
    {
        if (array_key_exists($key, $this->_data)) {
            return $this->_data[$key];
        } else {
            return false;
        }
    }
    
    /*
        Function: __set
        Set the value of the key in _data and add the key to _modifiedFields
    */
    public function __set($key, $value)
    {
        if ($key == $this->_keyField) {
			throw new Exception('can not set the key field'); 
			return FALSE;
		}
        $this->_data[$key] = $value;
        $this->_modifiedFields[] = $key;
        $this->_status = ($this->_status > -1) ? 0 : -1;
        
        return $this;
    }
    
	/*
		Function: __call
		Allows magic function calls such as findByFieldname
	*/
	public function __call($method, $args)
	{
		if (strpos($method, 'findBy') === 0) {
			array_unshift($args, strtolower(str_replace('findBy', '', $method)));
			call_user_func_array(array($this, 'findBy'), $args);
		}
	}
	
	/*
	    Function: getTableName
	    Returns the name of the table this class is mapped to
	*/
	public function getTableName()
	{
	    return $this->_tableName;
	}
	
	/*
	    Function: getKeyField
	    Returns the name of the key field column
	*/
	public function getKeyField()
	{
	    return $this->_keyField;
	}
	
    /*
        Function: isModified
        Checks to see if a given property has been modified
    */
    public function isModified($key)
    {
        return array_key_exists($key, $this->_modifiedFields);
    }

    /*
        Function: exists
        Determin whether an object is persisted in a database
    */
    public function exists()
    {
        return isset($this->_data['id']);
    }

    /*
        Function: load
        Load the row from the database that matches the _keyField to the id
        
        Arguments:
            id  - Value to match to the _keyField
    */
    public function load($id)
    {
        if ($this->preLoad() === FALSE) return FALSE;
        $result = $this->findBy($this->_tableName . '.' . $this->_keyField, $id);
        $this->postLoad();
        
        return $result;
    }

	/*
		Function: findBy
		Looks for a record with the key matching the value and populates the
        current instance with the data

		Arguments:
			key		- The column name to match
			value	- The value it needs to match
	*/
	public function findBy($key, $value)
	{
		$this->db->where($key, $value);
		$row = $this->db->get($this->_tableName)->row();
		
		if ($row) {
		    $this->populate($row);
			return $this;
		} else {
		    return NULL;
		}
	}

    /*
        Function: findRelated
        
        Arguments:
            object   - The name of the ActiveRecord object
    */
	public function getParent($obj)
	{
	    if ( ! array_key_exists($obj, $this->_parentObjects)) {
    	    $className = ucfirst($obj) . '_Record';
    	    $inst = new $className;
    	    $fk = $inst->getTableName() . '_id';
            $this->_parentObjects[$obj] = $inst->findBy('id', $this->$fk);
        }
        return $this->_parentObjects[$obj];
	}

    /*
        Function: populate
        Populates _data for this instnace from a database row object and sets
        the status as 1 (loaded from database, not modified)
    */
    public function populate($row)
    {
        foreach($row as $key => $value) {
            $this->_data[$key] = $value;
        }
        $this->_status = 1;
        
        return $this;
    }

    /*
        Function: _getModifiedData
        This returns only the data that has been modified
    */
    private function _getModifiedData()
    {
        foreach($this->_modifiedFields as $key) {
            $data[$key] = $this->_data[$key];
        }
        
        return $data;
    }

    /*
        Function: save
        Saves the current object to the database using values from _data where
        the key is in _modifiedFields. If the status is -1 (it is a new object)
        so we do an INSERT, if it is 0 (it was loaded from a database but
        properties have been modified) then we do an UPDATE. If the status is 1
        then it has been loaded from a database and no properties have been
        modified so we don't bother running a query
    */
    public function save()
    {
        switch($this->_status) {                
            case -1:
                if ($this->preInsert() === FALSE) return FALSE;
                if ($this->validateInsert() === FALSE) return FALSE;
                $this->db->insert($this->_tableName, $this->_getModifiedData());
                $this->_data[$this->_keyField] = $this->db->insert_id();
                $this->postInsert($this->db->insert_id());
                break;
            case 0:
                if ($this->preUpdate() === FALSE) return FALSE;
                if ($this->validateUpdate() === FALSE) return FALSE;
                $this->db->where($this->_keyField, $this->id);
                $this->db->update($this->_tableName, $this->_getModifiedData());
                $this->postUpdate();
                break;
            
            case 1:
                // If the status is 1 nothing is new, so just return TRUE
                return TRUE;
        }
        
        return $this;
    }

    /*
        Function: delete
        Deletes the current object from the database
    */
    public function delete()
    {
        if ($this->preDelete() === FALSE) return FALSE;
        $this->db->delete($this->_tableName, array($this->_keyField=>$this->_data[$this->_keyField]));
        $this->postDelete();
        unset($this);
    }
}

/*
	Class: ActiveRecordValidation
	Used to hold validation rules
*/
class ActiveRecordValidation {
	public static $insert_rules = array();
	public static $update_rules = array();
	public static $fields = array();
}